package org.unidata.mdm.rest.v1.meta.service.search;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.meta.type.search.ModelHeaderField;
import org.unidata.mdm.meta.type.search.ModelIndexType;
import org.unidata.mdm.rest.search.configuration.SearchRestConfigurationConstants;
import org.unidata.mdm.rest.search.converter.SearchResultToRestSearchResultConverter;
import org.unidata.mdm.rest.search.ro.SearchResultRO;
import org.unidata.mdm.rest.v1.meta.ro.search.MetaSearchQueryRO;
import org.unidata.mdm.rest.v1.meta.ro.search.MetaSearchResultRO;
import org.unidata.mdm.rest.v1.meta.service.AbstractMetaModelRestService;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.dto.SearchResultDTO;
import org.unidata.mdm.search.service.SearchService;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.query.SearchQuery;
import org.unidata.mdm.search.util.SearchUtils;

/**
 * Meta model search rest service
 *
 * @author Alexandr Serov
 * @since 24.11.2020
 **/
@Path(MetaSearchRestService.SERVICE_PATH)
@Consumes({"application/json"})
@Produces({"application/json"})
public class MetaSearchRestService extends AbstractMetaModelRestService {

    /**
     * This endpoint path.
     */
    public static final String SERVICE_PATH = "search";

    public static final String SERVICE_TAG = "search";

    @Autowired
    private SearchService searchService;

    /**
     * Searches meta data.
     *
     * @param fields the fields
     * @param text the text
     * @param count the count
     * @return hits
     */
    @GET
    @Operation(
        description = "Find registries and directories containing text in the given fields or return all if the search string is empty.",
        method = HttpMethod.GET,
        tags = SERVICE_TAG
    )
    public MetaSearchResultRO searchMeta(
        @Parameter(in = ParameterIn.QUERY) @QueryParam(SearchRestConfigurationConstants.SEARCH_PARAM_FIELDS) String fields,
        @Parameter(in = ParameterIn.QUERY) @QueryParam(SearchRestConfigurationConstants.SEARCH_PARAM_TEXT) String text,
        @Parameter(in = ParameterIn.QUERY) @QueryParam(SearchRestConfigurationConstants.SEARCH_PARAM_PAGE) @DefaultValue(SearchRestConfigurationConstants.DEFAULT_PAGE_NUMBER_VALUE) int page,
        @Parameter(in = ParameterIn.QUERY) @QueryParam(SearchRestConfigurationConstants.SEARCH_PARAM_COUNT) @DefaultValue(SearchRestConfigurationConstants.DEFAULT_OBJ_COUNT_VALUE) int count) {
        MetaSearchQueryRO query = new MetaSearchQueryRO();
        if (StringUtils.isNotBlank(fields)) {
            query.setFields(SearchUtils.getFields(fields));
        }
        query.setText(text);
        query.setPage(page);
        query.setCount(count);
        return executeSearchQuery(query);
    }

    @POST
    @Operation(
        description = "Find registries and directories containing text in the given fields or return all if the search string is empty.",
        method = HttpMethod.POST,
        tags = SERVICE_TAG
    )
    public MetaSearchResultRO searchMeta(MetaSearchQueryRO query) {
        return executeSearchQuery(query);
    }

    private MetaSearchResultRO executeSearchQuery(MetaSearchQueryRO query) {
        Objects.requireNonNull(query, "Query can't be null");
        List<String> searchFields = ObjectUtils.defaultIfNull(query.getFields(), Collections.emptyList());
        boolean isAllFetchSearch = StringUtils.isBlank(query.getText());
        MetaSearchResultRO result = new MetaSearchResultRO();
        if (isAllFetchSearch && searchFields.isEmpty()) {
            //because in ES we store un-normalized data
            result.setHits(Collections.emptyList());
            result.setFields(Collections.emptyList());
        } else {
            List<FieldsGroup> groupFormFields = new ArrayList<>();
            groupFormFields.add(FieldsGroup.or(searchFields.stream()
                .map(field -> FormField.exact(ModelHeaderField.ENTRY_TYPE, field))
                .collect(Collectors.toList())));
            if (!isAllFetchSearch) {
                groupFormFields.add(FieldsGroup.and(FormField.startsWith(ModelHeaderField.ENTRY_DESCRIPTION, query.getText())));
            }
            applyTo(result, search(SearchQuery.formQuery(groupFormFields), query.getPage(), query.getCount()));
        }
        return result;
    }

    private SearchResultRO search(SearchQuery searchQuery, int page, int count) {
        SearchResultDTO dto = searchService.search(SearchRequestContext.builder(ModelIndexType.MODEL, ModelIndexType.INDEX_NAME)
            .query(searchQuery)
            .returnFields(Arrays.stream(ModelHeaderField.values())
                .map(ModelHeaderField::getName)
                .collect(Collectors.toList()))
            .count(count)
            .runExits(true)
            .totalCount(true)
            .source(false)
            .skipEtalonId(true)
            .count(count > -1 ? count : 0)
            .page(page > 0 ? page - 1 : page)
            .build());
        return SearchResultToRestSearchResultConverter.convert(dto);
    }

    private void applyTo(MetaSearchResultRO target, SearchResultRO ro) {
        if (ro != null) {
            target.setHits(ro.getHits());
            target.setFields(ro.getFields());
            target.setMaxScore(ro.getMaxScore());
            target.setTotalCount(ro.getTotalCount());
        }
    }

    public SearchService getSearchService() {
        return searchService;
    }

    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }
}
